Design specification · build-ready · Support · Tickets
The exhaustive design specification for RiverSync's multi-party support conversation: the surface a customer uses to talk to the RiverSync service desk, the agent uses to work every tenant's tickets, and a servicing partner uses once they are brought onto a unit. It is specified as one reusable component — the same layout, command bar, thread and composer, reskinned per app by a single viewer setting — so Portal, Admin and Partners never diverge. Written so an implementer needs no further decisions: each region, state, class, icon and viewer rule is named.
The ticket communication system is a multi-party support conversation about one device. The customer opens a ticket (often from an alarm) and talks to the RiverSync service desk first line; RiverSync brings a servicing partner onto the thread when a unit's maintenance agreement makes them relevant. Every party works the same conversation — only what they may see and do changes.
One immersive page: a full-width context-aware command bar, then a list → thread → context-rail body that fills the height. The conversation parties are always customer + RiverSync, plus a partner once brought in. The surface is identical across the three apps; the differences are entirely captured by viewer.
The five W's of every ticket are visible in the list row before it is opened — who (the other parties' avatars + names), about what (subject + category), which device (id + status dot), when (last activity), what state (status badge + unread dot).
The component takes a single viewer prop. Everything app-specific — the shell chrome (App pill, nav, tenant), which parties are shown as "the others", the composer affordances and what is hidden — derives from it. There is no forked code per app, and no in-app "view as" switcher — each app deploys its own viewer-locked page through the shared mounter, so a customer only ever sees the customer view (in the Portal app), an agent the agent view (in Admin), a partner the partner view (in Partners).
| viewer | App / pill | Signed-in party | What this viewer can do |
|---|---|---|---|
| customer | Portal · --portal | Customer (Sanyodenki) | Open tickets from an alarm/device, reply to RiverSync (and the partner once present), request a partner on site, mark resolved. Never sees internal notes. |
| agent | Admin · --admin | RiverSync service desk | Works every tenant's tickets. Reply / internal note toggle, quick replies, bring in a servicing partner, schedule a visit, resolve/reopen. Sees everything. |
| partner | Partners · --partners | Servicing partner (Nera) | Sees only tickets they have been brought onto, scoped to their devices. Replies to RiverSync & the customer. Never sees RiverSync internal notes. |
A ticket starts as customer↔RiverSync. RiverSync (or, on request, the customer) brings a servicing partner onto the thread when the device's active maintenance agreement names one. This is a first-class event, not a re-route.
Agent brings in. The command bar / participant strip Bring in partner resolves the partner from agreementFor(device).active.partner, sets the ticket's partner, and writes a system event "RiverSync brought in {Partner} — servicing partner".
Customer requests. The same control for a customer reads Request partner and raises a request to RiverSync (toast); only RiverSync sets the partner.
Partner can't add a partner. The control is hidden for the partner viewer (they are the partner).
The UI never invents fields. Every rendered value maps to the ServiceTicket + TicketMessage model owned by the Support domain and defined in SPEC-ERD-PTL. Field names below are the presentation contract the prototype reads.
| Field | Key | Meaning & UI use |
|---|---|---|
| Id | PK | TKT-NNNN — list-row key, command-bar id, thread anchor. |
| DeviceId | FK | → Device. The ticket is always about one device; drives the device chip, rail, status dot. |
| CustomerId | FK | → the owning customer organization (a conversation party). |
| PartnerId | FK | → servicing partner · 0..1. Null until brought in; presence adds the third party (V-1). |
| Subject | Thread title — list row + thread header. | |
| Status | openin-progressresolved — status badge; resolved locks the composer. | |
| Priority | highmediumlow — priority badge (danger / warning / neutral). | |
| Category | incidentmaintenancefirmwarequestion — the category chip + icon. | |
| FromAlertId | FK | → Alert · 0..1 — the origin alarm; renders the Alarm linked chip + rail origin. |
| OpenedAt · Sla | Opened date + SLA line (e.g. "Awaiting first response") in the thread sub-header. | |
| LastActivityAt | + Unread — list timestamp and the unread dot / bold weight. |
| Field | Key | Meaning & UI use |
|---|---|---|
| TicketId | PF | Parent thread. |
| AuthorParty | customerriversyncpartner + author name. Determines bubble side per viewer (mine = right). | |
| Body | Markdown / text — rendered injection-safe by ChatMarkdown. | |
| Note | Boolean — an internal note: visible to riversync only; dashed warning bubble. | |
| Time · State | Per-message time + delivery state sendingsentdeliveredread (§9.3). | |
| Attachments | files · pictures · 0..n — each renders a FileAttachment / ImageSet card under the bubble. | |
| Alert · Slots · Appointment · Artifact | Optional embedded cards — device alert/alarm, a slot proposal, a confirmed visit, a shared .md. | |
| Kind | dayeventunread — non-message thread rows (divider · system event · unread marker). |
Below the shell chrome the page is a vertical stack: a full-width command bar, then a body grid that fills the remaining height — a fixed conversation list, the thread, and an optional context rail. Each column scrolls independently; the page itself does not scroll.
| Region | Sizing | Specification | |
|---|---|---|---|
| 1 | Command bar .pv-cmdbar | full width · 40px | Fixed at the top — the one chrome that never scrolls (height 40px, matching the shell top bar). Left: identity tile (Tickets + open-count + viewer label). Middle: context commands (hairline-divided). Right: search + primary Reply. |
| 2 | List column .pv-tk__list | 340px | Tools row (filter segment + count) pinned over an independently scrolling stack of ticket rows — the scrollbar is hidden (scrollbar-width:none) for a clean rail. |
| 3 | Category chip .pv-tkcat | auto | The ticket's category (incident / maintenance / firmware / question) with icon + tone, atop the subject. |
| 4 | Participant strip .pv-tkparty | auto | The other parties as avatar pills with presence; a dashed Bring in / Request partner chip when none. |
| 5 | Detail scroll .pv-tkscroll | fills · scrolls | The center scrolls as one region — the ticket header scrolls with the thread (only the command bar and composer stay fixed). Holds the header then the ChatThread (messages ascending; mine right, others left; cards under bubbles; day / event / unread / typing rows). Auto-scrolls to newest on open & send. |
| 6 | Composer .pv-tkcomp | auto · pinned | Control row (mode toggle / visibility + quick replies) over the DS ChatComposer; locked note when resolved. |
| 7 | Context rail .pv-tk__rail | 300px / 40px | Two states, toggled from the rail. Details (full — device, maintenance agreement, participants, origin) at 300px; and a minimized icon-rail glance at 40px — the collapsed left-sidebar width. At 40px every fact becomes one icon with a hover tooltip and tap-to-act: a tinted status glyph, a priority dot, the device, SLA, the agreement tier+partner, the origin alarm, then the participant avatars stacked. State persists in localStorage. Hidden under 1180px and in split layout. |
Grid: .pv-tk{display:grid; grid-template-columns:340px 1fr;}; .pv-tk--rail{…340px 1fr 300px;}. The page is immersive (.pv-tkpage--immersive): no page padding, fills below the 40px top bar. The center column is a single scroller (.pv-tkscroll) carrying header + thread; the command bar (40px) and composer are the only fixed chrome. Under 900px the list collapses and the thread takes over (master-detail) — see §11 responsive.
Adapted from the pipeline communications navbar: a full-width, square, hairline-divided strip of commands. It is context-aware — the commands depend on the selected ticket, its status and the viewer. The primary action sits at the right with search; when nothing is selected the middle shows the hint "Select a ticket to act on it."
| Command | Icon · style | Shown for | Behavior |
|---|---|---|---|
| Device | server | all | Opens the linked device. Label is the device id (e.g. MDC-2044). |
| Bring in partner | hub | agent · no partner | Resolves & sets the servicing partner; writes the bring-in event (V-1). |
| Request partner | hub | customer · no partner | Raises a request to RiverSync (toast) (V-2). |
| Schedule visit | calendar | agent · open | Opens the slot proposer for the servicing partner. |
| Resolve | success · ok | all · open | Sets status=resolved; writes a resolve event; locks the composer. |
| Reopen | refresh | all · resolved | Sets status=open; writes a reopen event; unlocks the composer. |
| Mark unread | email · icon | all | Re-flags the selected ticket unread without leaving it. |
| Reply | corner-down-left · primary | all · open | Right edge. Focuses the composer textarea. No toast. |
The left column is a pinned tools row (the Open / All / Resolved segment + an N shown count) over scrolling rows. One row is selected at a time and reflected in the thread.
| Element | Spec |
|---|---|
| Party avatars | Up to two overlapping 30px rs-av tiles — the other parties (presence dot when online). Two-initial, DS palette (rs-av-c0…c5). |
| Who + time | Top line: the other parties' short names joined by ·; a paperclip when the thread has attachments; relative time. |
| Subject + preview | Two-line subject (bold when unread) + one-line preview of the latest message. |
| Foot | Mono ticket id · device id with a status dot · the status badge (DS Badge, right-aligned). |
| Unread · selected | is-unread → accent dot + bold subject; is-active → accent-soft fill + 3px left bar. Selecting clears unread. |
The reader is a single scrolling region: a header that scrolls away with the conversation — a compact, horizontal two-row composition (row 1: category · subject · id/opened/SLA on the left, status & priority badges + actions on the right; row 2: the participant strip and the linked-context chips side by side) — then the thread. Only the command bar and the composer stay fixed. The thread is the DS ChatThread; messages map to its items.
| Region | Specification |
|---|---|
| Header sub-line | Status badge + priority badge + mono id + opened date + SLA (clock) — the facts of the ticket. |
| Participant strip .pv-tkparty | One pill per other party: 26px avatar (presence dot), short org name + role line ("Service desk · first line" / "Servicing partner" / "Customer", with · online when present). RiverSync's pill carries the accent tint. |
| Linked chips .pv-tklinks | Linked + a chip per related record: Device (always), Alarm (when from an alert, danger tone), Gold/Silver agreement (maintenance tone) → the partner. Each deep-links. |
| Message bubble | Mine = right (accent-soft), others = left (--surface-2). Avatar + name (others carry · {org}) + time; markdown body; cards beneath. |
| Cards under a bubble | ChatAlertCard (device alert/alarm) · ImageSet / FileAttachment · ArtifactCard (shared .md) · SlotPicker → AppointmentCard (visit). |
| System rows | day divider · event row (ticket opened / resolved / partner brought in) · unread marker. |
Avatars are the two-initial rs-av tiles from the communications system (square 2px, DS palette rs-av-c0…c5), sized via --s: 30px in list rows (overlapping −10px with a surface ring), 26px in the strip, 28px in the rail, sm in bubbles. Presence is the DS corner dot (rs-av__status online). This replaces the former single-letter circular avatar so every surface — list, strip, bubble, rail — reads identically and matches Pipeline.
The composer is the one entry point, and it adapts to the viewer. Around the DS ChatComposer (autosizing textarea, attach with progress, drag-and-drop, markdown preview, Enter-to-send) sits a control row that carries the operational affordances.
| Capability | Viewer | Specification |
|---|---|---|
| Reply vs internal note | agent | A segmented toggle Reply · Internal note. In note mode the placeholder, the visibility line and the sent bubble all switch — the message is stored Note=true and never shown to customer/partner (§4 Note rule). A live visibility line states exactly who can see the next message. |
| Quick replies (canned) | agent | A Quick replies popover of curated, sentence-case presets (Acknowledge · Ask for a reading · Dispatching a partner · Confirm before close). Picking one sends it immediately as a normal reply. |
| Attachments | all | Files & pictures stage with progress chips (drag-and-drop supported); each posts as a FileAttachment / ImageSet card under the bubble. Images open a lightbox. |
| Device alert / alarm cards | all | A ticket opened from an alarm carries the ChatAlertCard inline (severity tone, alarm ping) with a View deep-link to the alert. |
| Visit scheduling | agent | The composer's calendar tool (reply mode only) proposes visit slots (SlotPicker); a confirmed visit renders an AppointmentCard in-thread. |
| Visibility line | customer · partner | Above the composer: "Visible to {parties}" — so a sender always knows the audience. Resolved tickets show the locked note instead. |
Two live signals keep the conversation feeling staffed.
Presence. Each participant avatar (strip, list, rail) shows the DS online dot when that party is present — the RiverSync service desk reads · online so a customer knows someone is there.
Typing. After the viewer sends a reply, a transient ChatTyping row from the responding party appears at the foot of the thread (the service desk for customer/partner; the customer for the agent) and clears after a short beat — the indicator the DS provides, gated to reply (not note) sends and honoring reduced motion.
Every status change and message write runs the same way regardless of which control fired it (command bar, header menu or composer), so behavior is identical across the surface.
| Action | Result | Toast |
|---|---|---|
| Reply | Append message; (reply mode) trigger typing | — (no toast) |
| Internal note | Append Note=true message (agent) | — (no toast) |
| Quick reply | Send the chosen preset immediately | — (no toast) |
| Resolve | status=resolved; resolve event; lock composer | "Ticket resolved · TKT status set to Resolved." (success) |
| Reopen | status=open; reopen event; unlock | "Ticket reopened…" (info) |
| Bring in partner | Set partner; bring-in event | "Partner brought in · {Partner} added to this ticket." (info) |
| Mark unread | Re-flag the ticket unread | "Marked unread…" (info) |
A resolved ticket replaces the composer with a locked note — "This ticket is resolved. Reopen it to keep talking." The command bar swaps Resolve for Reopen and drops the primary Reply. Reopening restores the open state and the composer.
No selection → the thread shows .pv-tk__empty (ticket icon, "Select a ticket"). A filter that matches nothing → .pv-tkrow__none (inbox icon, "No tickets in this view."). Layout reserves its dimensions; the surface never reflows on data arrival (mirrors the charts rule).
Delivery state advances sendingsentdeliveredread with the DS tick (single → double → accent double). Receipts are derived, not hand-set: one of my messages reads as read once any later message from another party exists; the newest unanswered one shows delivered. Receipts show only on my own outgoing messages — never on incoming. A failed send shows a danger state with Retry; it never silently disappears.
List + thread reserve their dimensions; skeleton rows / quiet placeholder. No reflow when data lands.
Composer → locked note; bar → Reopen; thread read-only until reopened.
Participant strip shows the dashed Bring in / Request partner chip (by viewer).
Optimistic message rolls back to a danger chip with Retry; never dropped.
Every element resolves to a RiverSync DS v2 component, class, icon or token. The surface is a React page composing the DS chat layer; the pv-tk* / pv-cmdbar* classes are the Portal-local layer in portal.css, themed entirely through DS tokens.
| Element | Class / component | DS basis |
|---|---|---|
| App pill (top bar) | rs-portalbadge--portal / --admin / --partners | Canonical app badge per viewer (shell-chrome.css). Mandatory. |
| Shell | AppShell (hub / admin) · vanilla shell (partners) | The DS shell; the partner pill is re-tinted where AppShell ships no partners config (see §10.1). |
| Thread | ChatThread + ChatMessage / ChatEvent / ChatDay / ChatUnread / ChatTyping | DS chat system — bubbles, delivery ticks, typing dots. |
| Composer | ChatComposer | DS composer — attach + progress, drag-drop, markdown, schedule hook. |
| Cards | ChatAlertCard · FileAttachment · ImageSet · ArtifactCard · SlotPicker · AppointmentCard | DS card-under-bubble idiom. |
| Avatar | rs-av rs-av-c0…c5 + rs-av__status | DS avatar tile (square, two-initial) + presence dot. |
| Status / priority | Badge (tone) | DS status badge — 2px radius, soft tint, sentence case. |
| Command buttons | pv-cmd pv-cmd--primary / --ok / --icon | Command-bar buttons over DS tokens (the pipeline pl-cmd idiom). |
| Segments | rs-seg rs-seg--sm | DS segmented control (filter + reply/note toggle). |
| Toast | PVToast(title, msg, kind) | Portal toast — success / info / warning. |
| Icons | <Icon name="…"/> | Proprietary icon system (the G geometry table) — never a third-party set. |
Icons in use: ticket · server · hub · calendar · success · refresh · email · corner-down-left · search · alert · wrench · chevron-right · more-horizontal · paperclip · image · file-text · chat · lock · eye · check · plus · inbox · clock. Any new glyph is added to the DS G table first, never drawn inline.
The whole point of this spec is one surface, embedded by every app. These rules keep it that way.
One engine, embedded per app. The surface is the single component PV.Tickets, mounted through the shared helper PV.mountTicketApp (ticket-app.jsx). Each app deploys its own page (Portal · Admin · Partners); they embed the engine, they do not copy it. A change to the thread, command bar or composer lands once and every app inherits it.
Configure by props, not forks. An embedding app passes only: viewer, the ticket data + onAddUpdate / onTicketChange handlers, me (signed-in party), go (navigation), toast, and presentation flags (layout · bubbles · density). No app reaches inside the component.
The shell is the app's, the surface is shared. Each app wraps the surface in its own shell with its own App pill (Portal · Admin · Partners). The surface renders only the page content (.rs-page), never chrome.
Visibility is centralized. Internal-note hiding, party filtering and partner scoping are computed inside the component from viewer — never re-implemented per app, so a customer client can never receive a note.
Layout flags, not layout forks. layout="rail" adds the context rail (Portal/Admin detail); layout="split" drops it (embedded/compact). Both come from the one component.
| Breakpoint | Behavior |
|---|---|
| > 1180px | Full three-column (list · thread · rail) in rail layout; the rail toggles between full Details (300px) and a minimized icon-rail glance at 40px — the collapsed left-sidebar width — every fact an icon + tooltip (persisted). |
| ≤ 1180px | Rail hidden; list + thread. |
| ≤ 1024px | Command-bar identity sub-label + search hide; commands stay. |
| ≤ 900px | Command labels collapse to icons; list collapses, thread takes over (master-detail). |
| Concern | Requirement |
|---|---|
| Semantics | List rows are buttons (selected reflected); the thread is a labelled log; the unread dot and presence dot carry text equivalents; status/priority are badge text, not color-only. |
| Focus | 3px soft ring (--ring) on :focus-visible; Reply moves focus to the composer textarea; menus close on outside-click / Escape. |
| Color | Category, status, priority and note all pair an icon or text with their tint — never color alone. |
| Motion | Typing dots and delivery ticks honor prefers-reduced-motion; no decorative looping. |
| Hit targets | Rows ≥ 38px; command/icon buttons ≥ 40px; composer Send comfortably tappable. |
The prototype is the source of truth for look & behavior; these are the deltas a production build must close. None changes the model.
| Area | As-built | Production target |
|---|---|---|
| Partner shell | re-tinted pill | First-class partners portal in AppShell (§10.1). |
| Typing & presence | simulated | Live presence + typing over the realtime hub. |
| Read receipts | derived from thread | Real per-message delivery/read receipts pushed from the server. |
| Send / status | local state | Persist TicketMessage / status, write events, emit domain events. |
| Quick replies | fixed presets | Editable canned-reply library, per-agent & per-team. |
| Attachments | placeholder cards | Real upload + image lightbox + file download. |
| Scheduling | toast / cards | Live slot proposal → partner confirm → calendar write. |
| This spec | Traces to |
|---|---|
| Whole document | PTL-4 (Portal PRD) — tickets as a multi-party support conversation; reused by SPEC-APP-PAR · SPEC-APP-ADM. |
| Data model (§4) | ServiceTicket · TicketMessage (SPEC-ERD-PTL), Support domain (SPEC-DDD-SUP). |
| Bring-in & agreements (§3.1) | Maintenance agreement & partner scope (SPEC-PRD-MNT, SPEC-ERD-PAR). |
| Process (§9) | SPEC-PWF-INC · ticket lifecycle in SPEC-DWF-SUP. |
| Notes / visibility (§4, §8) | Federation roles & tenant visibility (SPEC-PRD-FED). |
| Version | Date | Changes |
|---|---|---|
| 1.0 | 29 Jun 2026 | First issue — the ticket communication system specified as one reusable surface deployed as a dedicated, viewer-locked page inside each app (no "view as" switcher): Portal (customer) · Admin (RiverSync agent) · Partners (servicing partner). Scope & distinctions (§1–2), the three viewers & partner bring-in (§3), the ServiceTicket/TicketMessage data (§4), layout & grid with annotated wireframe (§5), the context-aware command bar + list/row (§6), thread, parties & the pipeline avatar idiom (§7), communicating in the chat — internal notes, quick replies, attachments, alert cards, scheduling, presence & typing (§8), interactions/states & read receipts (§9), design-system mapping incl. the partners-badge note (§10), reuse rules & responsive (§11), as-built vs target, open questions / proposed requirements, traceability (§12–14). Brought the avatar design and the command-bar navigation from the Pipeline communications system into the ticket surface, and corrected the shared shell brand to the real RiverSync logo/mark vectors. A presentation view over a settled model — no new entities, fields, events or rules; proposed reuse requirement logged. Added to the Design specs family (SPEC-UX-TKT). |